--[[

    Cry of Fear Gameplay.
    Version 04/20/2019.

    Description:
    Cry of Fear gameplay system for CSO Studio.


        MIT License

        Copyright (c) 2019 Anggara Yama Putra

        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:

        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.

        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.

]]


-- Local variables.
local debug = COF.Debug;
local module = COF.Game;
local vector = COF.Vector;
local modulePlayer = module.Player;
local moduleEvent = module.Event;
local syncvalue = COF.SyncValue;
local enum = COF.Enum;
local plrBtnPressingBit = {};
local plrLastJumpTapTime = {};
local plrNextStaminaRecoverTime = {};
local plrNextDodgeTime = {};
local plrNextCutsceneResetTime = {};
local plrBtnLastDodgeTap = {};
local plrLastDodgeTapTime = {};
local plrDoDoubleTapDodge = {};
local plrLastVelZ = {};
for id = 1, COF.MAX_PLAYER
do
    plrBtnPressingBit[id] = 0;
    plrLastJumpTapTime[id] = 0;
    plrNextStaminaRecoverTime[id] = 0;
    plrNextDodgeTime[id] = 0;
    plrNextCutsceneResetTime[id] = 0;
    plrBtnLastDodgeTap[id] = 0;
    plrLastDodgeTapTime[id] = 0;
    plrLastVelZ[id] = 0;
    plrDoDoubleTapDodge[id] = false;
end


-- Init SyncValue(s).
syncvalue.Chapter.Instance = Game.SyncValue.Create( syncvalue.Chapter.NAME );
syncvalue.Chapter.Instance.value = nil;
syncvalue.Objective.Instance = Game.SyncValue.Create( syncvalue.Objective.NAME );
syncvalue.Objective.Instance.value = nil;
syncvalue.Phone.Available.Instance = Game.SyncValue.Create( syncvalue.Phone.Available.NAME );
syncvalue.Phone.Available.Instance.value = true;
syncvalue.Phone.Message.Instance = Game.SyncValue.Create( syncvalue.Phone.Message.NAME );
syncvalue.Phone.Message.Instance.value = nil;
syncvalue.ScreenFade.Instances = {};
syncvalue.Health.Instances = {};
syncvalue.MaxHealth.Instances = {};
syncvalue.Stamina.Instances = {};
syncvalue.Cutscene.Instances = {};
syncvalue.Subtitle.Instances = {};
syncvalue.Hint.Instances = {};
syncvalue.Prologue.Instances = {};
syncvalue.Epilogue.Instances = {};
syncvalue.Credit.Instances = {};
for i = 1, COF.MAX_PLAYER
do
    syncvalue.ScreenFade.Instances[i] = Game.SyncValue.Create( string.format( syncvalue.ScreenFade.NAME , i ) );
    syncvalue.ScreenFade.Instances[i].value = nil;
    syncvalue.Health.Instances[i] = Game.SyncValue.Create( string.format( syncvalue.Health.NAME , i ) );
    syncvalue.MaxHealth.Instances[i] = Game.SyncValue.Create( string.format( syncvalue.MaxHealth.NAME , i ) );
    syncvalue.Stamina.Instances[i] = Game.SyncValue.Create( string.format( syncvalue.Stamina.NAME , i ) );
    syncvalue.Cutscene.Instances[i] = Game.SyncValue.Create( string.format( syncvalue.Cutscene.NAME , i ) );
    syncvalue.Cutscene.Instances[i].value = false;
    syncvalue.Subtitle.Instances[i] = Game.SyncValue.Create( string.format( syncvalue.Subtitle.NAME , i ) );
    syncvalue.Subtitle.Instances[i].value = nil;
    syncvalue.Hint.Instances[i] = Game.SyncValue.Create( string.format( syncvalue.Hint.NAME , i ) );
    syncvalue.Hint.Instances[i].value = nil;
    syncvalue.Prologue.Instances[i] = Game.SyncValue.Create( string.format( syncvalue.Prologue.NAME , i ) );
    syncvalue.Prologue.Instances[i].value = false;
    syncvalue.Epilogue.Instances[i] = Game.SyncValue.Create( string.format( syncvalue.Epilogue.NAME , i ) );
    syncvalue.Epilogue.Instances[i].value = nil;
    syncvalue.Credit.Instances[i] = Game.SyncValue.Create( string.format( syncvalue.Credit.NAME , i ) );
    syncvalue.Credit.Instances[i].value = false;
end


-- Gets a player index.
function module:GetPlayerIndex( object )
    if object == nil then return nil; end
    local numerik = tonumber( object );
    if numerik ~= nil and numerik >= 1 and numerik <= COF.MAX_PLAYER then return numerik; end
    if object.index ~= nil then return object.index; end
    return nil;
end


-- Gets player's movement speed.
function modulePlayer:GetSpeed( player )
    local index = module:GetPlayerIndex( player );
    if index == nil then return -1 end;

    local x,y = player.velocity.x , player.velocity.y;
    return math.sqrt( x*x + y*y );
end


-- Is stamina recovery is on cooldown?
function modulePlayer:IsStaminaRecoveryCooldown( player )
    local index = module:GetPlayerIndex( player );
    if index == nil then return false end;

    return Game:GetTime() < plrNextStaminaRecoverTime[ index ];
end


-- Returns the smallest stamina recovery rates.
function module:GetSmallestStaminaRecoveryRate()
    return module.STAMINA_RECOVER_STANDING; --math.min( module.STAMINA_RECOVER_STANDING , module.STAMINA_RECOVER_CROUCH );
end


-- Gets player's stamina.
function modulePlayer:GetStamina( player )
    local index = module:GetPlayerIndex( player );
    if index == nil then return -1 end;

    local value = syncvalue.Stamina.Instances[ index ].value;
    if value == nil then value = -2 end;
    return value;
end


-- Is player able to sprint?
function modulePlayer:CanSprint( player )
    local index = module:GetPlayerIndex( player );
    if index == nil then return false end;

    local speed = self:GetSpeed( player );
    return module.SPRINT_ENABLED 
            and self:GetStamina( player ) >= module.STAMINA_SPRINT_COST 
            and speed >= COF.WALK_SPEED - 1 
            and speed <= module.SPRINT_SPEED 
            --and player.velocity.z == 0; -- on ground.
end


-- Is player able to dodging?
function modulePlayer:CanDodge( player )
    local index = module:GetPlayerIndex( player );
    if index == nil then return false end;

    local speed = self:GetSpeed( player );
    return module.DODGE_ENABLED
            and self:GetStamina( player ) >= module.STAMINA_DODGE_COST 
            and speed >= COF.CROUCH_SPEED + 1 
            and speed < COF.MAX_SPEED 
            and player.velocity.z == 0 -- on ground.
            and plrNextDodgeTime[ index ] < Game:GetTime();
end


-- Checks whether player's cutscene mode is active.
function modulePlayer:IsCutscene( player )
    local index = module:GetPlayerIndex( player );
    if index == nil then return false end;

    return ( syncvalue.Cutscene.Instances[ index ].value and true or false );
end


-- Checks whether phone is available.
function module:IsPhoneAvailable()
    return ( syncvalue.Phone.Available.Instance.value and true or false );
end


-- Checks whether prologue text is visible.
function module:IsPrologueVisible()
    return ( syncvalue.Prologue.Instance.value and true or false );
end


-- Checks whether credits text is visible.
function module:IsCreditVisible()
    return ( syncvalue.Credit.Instance.value and true or false );
end


-- Gets chapter part and title.
function module:GetChapter()
    local value = syncvalue.Chapter.Instance.value;
    if value == nil then return nil, nil; end
    
    local chapter = string.explode( value , syncvalue.Chapter.Separator );
    return chapter[1] , chapter[2];
end


-- Gets player's objective.
function module:GetObjective()
    return syncvalue.Objective.Instance.value;
end


-- Gets player's subtitle text.
function modulePlayer:GetSubtitle( player )
    local index = module:GetPlayerIndex( player );
    if index == nil then return nil end;

    return syncvalue.Subtitle.Instances[ index ].value;
end


-- Gets last player's hint text.
function modulePlayer:GetHint( player )
    local index = module:GetPlayerIndex( player );
    if index == nil then return nil end;

    return syncvalue.Hint.Instances[ index ].value;
end


-- Gets phone message text id.
function module:GetPhoneMessage()
    return syncvalue.Phone.Message.Instance.value;
end


-- Gets epilogue text.
function module:GetEpilogueText()
    return syncvalue.Epilogue.Instance.value;
end


-- Draws screen fade to player.
function modulePlayer:DrawScreenFade( player , fademode , faderate, r , g , b , a )
    local index = module:GetPlayerIndex( player );
    if index == nil then return end;

    local time = Game:GetTime();
    fademode = tostring( fademode );
    faderate = tostring( faderate );
    r = tostring( r );
    g = tostring( g );
    b = tostring( b );
    a = tostring( a );

    --[[
    debug:print( "DrawScreenFade()" );
    debug:print( "index= " .. tostring( index ) );
    debug:print( "time= " .. time );
    debug:print( "fademode= " .. fademode );
    debug:print( "faderate= " .. faderate );
    debug:print( "r= " .. r );
    debug:print( "g= " .. g );
    debug:print( "b= " .. b );
    debug:print( "a= " .. a );
    ]]

    syncvalue.ScreenFade.Instances[ index ].value = string.format( "%f;%s;%s;%s;%s;%s;%s", time, fademode, faderate, r, g, b, a );
end


-- Sets player's cutscene mode.
function modulePlayer:SetCutscene( player , isOn , duration )
    local index = module:GetPlayerIndex( player );
    if index == nil then return end;

    --if isOn and plrNextCutsceneResetTime[ index ] > 0 then return; end -- wait until timer is expired.

    debug:print( "SetCutscene()" );
    debug:print( "index= " .. tostring( index ) );
    debug:print( "isOn= " .. tostring( isOn ) );

    if tonumber( player ) ~= nil
    then
        player = Game.Player.Create( player );
    end

    if player ~= nil
    then
        -- Freeze up!
        player.velocity= { x = 0 , y = 0 , z = 0 };
    end

    local time = Game:GetTime();
    syncvalue.Cutscene.Instances[ index ].value = isOn;

    -- Timed cutscene.
    if isOn and duration ~= nil and duration > 0
    then
        plrNextCutsceneResetTime[ index ] = time + duration;
        debug:print( "time= " .. plrNextCutsceneResetTime[ index ] );
    end
end


-- Sets chapter title.
function module:SetChapter( part , title )
    debug:print( "SetChapter()" );
    
    part , title = tostring(part) , tostring(title);
    debug:print( "part= " .. part );
    debug:print( "title= " .. title );

    syncvalue.Chapter.Instance.value = string.format( "%s" .. syncvalue.Chapter.Separator .. "%s" , part , title );
end


-- Sets player's stamina.
function modulePlayer:SetStamina( player , value, dontDelay )
    local index = module:GetPlayerIndex( player );
    if index == nil then return end;

    -- Clamp it.
    if value > COF.STAMINA_MAX then value = COF.STAMINA_MAX;
    elseif value < 0 then value = 0 end;

    syncvalue.Stamina.Instances[ index ].value = value;
    local regenStamina = module:GetSmallestStaminaRecoveryRate();
    --player.maxarmor = (value > 1 or value < regenStamina) and math.floor( value ) or math.ceil( value );

    if not dontDelay
    then
        -- Stamina depleted? don't regen until next time.
        if value < regenStamina and plrNextStaminaRecoverTime[ index ] == 0
        then
            plrNextStaminaRecoverTime[ index ] = Game:GetTime() + module.STAMINA_RECOVER_DEPLETED_COOLDOWN;
        end
    end
end


-- Sets player's objective.
function module:SetObjective( text )
    debug:print( "SetObjective()" );
    debug:print( "text= " .. tostring(text) );

    syncvalue.Objective.Instance.value = text;
end


-- Sets player's subtitle text.
function modulePlayer:SetSubtitle( player , subID )
    local index = module:GetPlayerIndex( player );
    if index == nil then return end;

    debug:print( "SetSubtitle()" );
    debug:print( "index= " .. tostring( index ) );
    debug:print( "subID= " .. tostring( subID ) );

    syncvalue.Subtitle.Instances[ index ].value = subID;
end


-- Enqueues the text into the player's hint queue list.
function modulePlayer:EnqueueHint( player , txt )
    local index = module:GetPlayerIndex( player );
    if index == nil then return end;

    debug:print( "EnqueueHint()" );
    debug:print( "index= " .. tostring( index ) );
    debug:print( "txt= " .. tostring( txt ) );

    syncvalue.Hint.Instances[ index ].value = txt;
end


-- Shown prologue scene to the player.
function modulePlayer:ShowPrologue( player , isOn )
    local index = module:GetPlayerIndex( player );
    if index == nil then return end;

    syncvalue.Prologue.Instances[ index ].value = isOn;
end


-- Shown epilogue scene to the player with given text.
function modulePlayer:ShowEpilogue( player , txt )
    local index = module:GetPlayerIndex( player );
    if index == nil then return end;

    syncvalue.Epilogue.Instances[ index ].value = txt;
end


-- Shown credits scene to the player.
function modulePlayer:ShowCredit( player , isOn )
    local index = module:GetPlayerIndex( player );
    if index == nil then return end;

    syncvalue.Credit.Instances[ index ].value = isOn;
end


-- Draws screen fade to all players.
function module:DrawScreenFade( fademode , faderate, r , g , b , a )
    for i = 1, COF.MAX_PLAYER
    do
        modulePlayer:DrawScreenFade( i , fademode , faderate, r , g , b , a );
    end
end


-- Sets all player's cutscene mode.
function module:SetCutscene( isOn , duration )
    for i = 1, COF.MAX_PLAYER
    do
        modulePlayer:SetCutscene( i , isOn , duration );
    end
end


-- Sets all player's subtitle text.
function module:SetSubtitle( subID )
    for i = 1, COF.MAX_PLAYER
    do
        modulePlayer:SetSubtitle( i , subID );
    end
end


-- Enqueues the text into all player's hint queue list.
function module:EnqueueHint( txt )
    for i = 1, COF.MAX_PLAYER
    do
        modulePlayer:EnqueueHint( i , txt );
    end
end


-- Shown prologue scene to all players.
function module:ShowPrologue( isOn )
    for i = 1, COF.MAX_PLAYER
    do
        modulePlayer:ShowPrologue( i , isOn );
    end
end


-- Shown epilogue scene to all players with given text.
function module:ShowEpilogue( txt )
    for i = 1, COF.MAX_PLAYER
    do
        modulePlayer:ShowEpilogue( i , txt );
    end
end


-- Shown credits scene to all players.
function module:ShowCredit( isOn )
    for i = 1, COF.MAX_PLAYER
    do
        modulePlayer:ShowCredit( i , isOn );
    end
end


-- Sets phone availability.
function module:SetPhoneAvailability( isAvailable )
    syncvalue.Phone.Available.Instance.value = isAvailable;
end


-- Sets phone message text id.
function module:SetPhoneMessage( msgID )
    syncvalue.Phone.Message.Instance.value = msgID;
    Game.SetTrigger( module.PHONE_MESSAGE_NEW_TRIGGER_NAME , false );
    Game.SetTrigger( module.PHONE_MESSAGE_NEW_TRIGGER_NAME , true );
end


-- Updates player's double-tap dodge.
function modulePlayer:UpdateDoubleTapDodge( player , value )
    if not module.DODGE_TAP_ENABLED then return end;
    local index = module:GetPlayerIndex( player );
    if index == nil then return end;

    local time = Game:GetTime();
    local compare = time - plrLastDodgeTapTime[ index ];
    if plrBtnLastDodgeTap[ index ] == value 
        and compare >= (module.DODGE_TAP_INTERVAL_MIN/1000) 
        and compare <= (module.DODGE_TAP_INTERVAL_MAX/1000)
    then
        plrDoDoubleTapDodge[ index ] = true;
        value = 0; -- remove old tap.
    end

    plrBtnLastDodgeTap[ index ] = value;
    plrLastDodgeTapTime[ index ] = time;
end


-- Updates player's HP and max. HP syncvalue.
function modulePlayer:UpdateHealthDisplay( player )
    local index = module:GetPlayerIndex( player );
    if index == nil then return end;

    local player = Game.Player.Create( index );
    if player == nil then return end;

    local maxhp = player.maxhealth;
    if syncvalue.MaxHealth.Instances[ index ].value ~= maxhp
    then
        syncvalue.MaxHealth.Instances[ index ].value = maxhp;
    end
    local hp = player.health;
    if syncvalue.Health.Instances[ index ].value ~= hp
    then
        syncvalue.Health.Instances[ index ].value = hp;
    end
end


-- OnPlayerSpawn hook.
function module:OnPlayerSpawn( player )
    local index = player.index;
    plrNextStaminaRecoverTime[index] = 0;
    plrNextDodgeTime[index] = 0;

    -- Reset player's stamina.
    modulePlayer:SetStamina( player , COF.STAMINA_MAX );

    -- Updates hp and max. hp.
    modulePlayer:UpdateHealthDisplay( player );
end


-- OnPlayerAttack hook.
function module:OnPlayerAttack( victim , attacker , dmg , weapon , hitbox )
    if modulePlayer:IsCutscene( victim )
    then
        return 0; -- Invicible during cutscene.
    end

    -- Updates hp and max. hp.
    modulePlayer:UpdateHealthDisplay( player );
end


-- OnUpdate hook.
function module:OnUpdate( time )
    for index = 1, COF.MAX_PLAYER
    do
        local player = Game.Player.Create( index );
        if player ~= nil
        then
            local doModifyVelocity = false;
            local newSpeed = 250;
            local regenStamina = self.STAMINA_RECOVER_STANDING;
            local oldStamina = modulePlayer:GetStamina( player );
            local newStamina = oldStamina < COF.STAMINA_MAX and oldStamina + regenStamina or oldStamina;
            local btnPress = plrBtnPressingBit[ index ];
            local csTimer = plrNextCutsceneResetTime[ index ];

            -- Cutscene timer is expired.
            if csTimer > 0 and csTimer < time
            then
                plrNextCutsceneResetTime[ index ] = 0;
                modulePlayer:SetCutscene( player , false );
            end

            -- Freeze up when on cutscene.
            if modulePlayer:IsCutscene( player ) and module.CUTSCENE_FREEZE_MOVEMENT_ENABLED
            then
                doModifyVelocity = true;
                newSpeed = 0;
            end
            
            -- Sprinting.
            if btnPress == (enum.BUTTON.SHIFT | enum.BUTTON.W)
            then
                if modulePlayer:CanSprint( player )
                then
                    doModifyVelocity = true;
                    newSpeed = self.SPRINT_SPEED;
                    newStamina = oldStamina - self.STAMINA_SPRINT_COST;

                    -- Counter-slowdown of walking.
                    if player.velocity.z == 0
                    then
                        newSpeed = newSpeed + 160;
                    end
                end

            elseif ( btnPress & enum.BUTTON.SHIFT ~= 0
                        and (
                            btnPress & enum.BUTTON.W ~= 0
                            or btnPress & enum.BUTTON.S ~= 0
                            or btnPress & enum.BUTTON.A ~= 0
                            or btnPress & enum.BUTTON.D ~= 0
                        )
                    ) -- SHIFT + Dodge.
                    or
                    plrDoDoubleTapDodge[index] -- Double-tap dodging.
            then
                if modulePlayer:CanDodge( player )
                then
                    plrNextDodgeTime[index] = Game:GetTime() + self.DODGE_COOLDOWN;

                    doModifyVelocity = true;
                    newSpeed = self.DODGE_SPEED;
                    newStamina = oldStamina - self.STAMINA_DODGE_COST;
                end
            end

            -- Boosts player velocity.
            if doModifyVelocity
            then
                local y = vector:VectorAngles( player.velocity ).y;
                local range = vector:Range90Angle( y );
                local quadrant = vector:GetQuadrant( y );

                local velX, velY = (1-range), range;
                if quadrant == 1 or quadrant == 3
                then
                    velX, velY = velY, velX;
                end

                velX = velX * newSpeed;
                velY = velY * newSpeed;

                if quadrant == 2 or quadrant == 3 then
                    velX = velX * -1;
                end
                if quadrant == 3 or quadrant == 4 then
                    velY = velY * -1;
                end

                player.velocity = { x = velX , y = velY };
            end

            if modulePlayer:IsStaminaRecoveryCooldown( player ) == false
            then
                plrNextStaminaRecoverTime[index] = 0;

                if newStamina ~= oldStamina
                then
                    -- Sets new stamina value.
                    modulePlayer:SetStamina( player , newStamina );
                end
            end

            -- Updates hp and max. hp.
            modulePlayer:UpdateHealthDisplay( player );
        end
        
        -- Updates last player z-axis velocity.
        plrLastVelZ[ index ] = (player ~= nil) and player.velocity.z or 0;

        -- Clears button pressing bitflags.
        plrBtnPressingBit[ index ] = 0;

        -- Clears double-tap dodge.
        plrDoDoubleTapDodge[index] = false;
    end
end


-- OnPlayerSignal hook.
function module:OnPlayerSignal( player, signal )
    -- Cutscene is changed on client's side.
    if signal == enum.SIGNAL.CUTSCENE_ON or signal == enum.SIGNAL.CUTSCENE_OFF
    then
        debug:print( "Cutscene signal received." );
        moduleEvent:OnCutscene( player , (signal == enum.SIGNAL.CUTSCENE_ON) );
    end

    -- Prologue is changed on client's side.
    if signal == enum.SIGNAL.PROLOGUE_ON or signal == enum.SIGNAL.PROLOGUE_OFF
    then
        debug:print( "Prologue signal received." );
        moduleEvent:OnPrologue( player , (signal == enum.SIGNAL.PROLOGUE_ON) );
    end

    -- Epilogue is changed on client's side.
    if signal == enum.SIGNAL.EPILOGUE_ON or signal == enum.SIGNAL.EPILOGUE_OFF
    then
        debug:print( "Epilogue signal received." );
        moduleEvent:OnEpilogue( player , (signal == enum.SIGNAL.EPILOGUE_ON) );
    end

    -- Credits is changed on client's side.
    if signal == enum.SIGNAL.CREDIT_ON or signal == enum.SIGNAL.CREDIT_OFF
    then
        debug:print( "Credits signal received." );
        moduleEvent:OnCredit( player , (signal == enum.SIGNAL.CREDIT_ON) );
    end
    
    -- Don't do anything while on cutscene.
    if modulePlayer:IsCutscene( player ) then return; end

    local index = player.index;
    local btnPress = plrBtnPressingBit[index];

    -- Disabled because we can't detect player ducking yet. --Anggara_nothing
    -- Crouch.
    --[[
    if signal == enum.SIGNAL.KEY_CTRL
    then
        plrBtnPressingBit[index] = btnPress | enum.BUTTON.CTRL;
    end
    ]]

    -- Jump.
    if signal == enum.SIGNAL.KEY_SPACE
    then
        local time = Game:GetTime();
        local compare = time - plrLastJumpTapTime[ index ];
        local velZ = player.velocity.z;

        -- Newly pressed SPACE key without holding it.
        if compare > (COF.FASTEST_RECEIVED_DOUBLETAP_INPUT_TIME/1000) and plrLastVelZ[index] == 0
        then
            if velZ >= COF.JUMP_VELOCITY_TOLERANCE_MIN
                and velZ <= COF.JUMP_VELOCITY_TOLERANCE_MAX
            then
                -- Get jump stamina penalty.
                local newStamina = modulePlayer:GetStamina( player ) - self.STAMINA_JUMP_COST;
                modulePlayer:SetStamina( player , newStamina );
            end
        end

        plrBtnPressingBit[index] = btnPress | enum.BUTTON.SPACE;
        plrLastJumpTapTime[ index ] = time;
        plrLastVelZ[index] = velZ;
    end

    -- Sprint.
    if signal == enum.SIGNAL.SPRINT
    then
        plrBtnPressingBit[index] = enum.BUTTON.SHIFT | enum.BUTTON.W;
        return; -- ignores other keys.
    end

    if signal == enum.SIGNAL.KEY_SHIFT
    then
        plrBtnPressingBit[index] = btnPress | enum.BUTTON.SHIFT;
    end

    if signal == enum.SIGNAL.KEY_W
    then
        plrBtnPressingBit[index] = btnPress | enum.BUTTON.W;

        modulePlayer:UpdateDoubleTapDodge( player , enum.BUTTON.W );
    end

    if signal == enum.SIGNAL.KEY_S
    then
        plrBtnPressingBit[index] = btnPress | enum.BUTTON.S;

        modulePlayer:UpdateDoubleTapDodge( player , enum.BUTTON.S );
    end

    if signal == enum.SIGNAL.KEY_A
    then
        plrBtnPressingBit[index] = btnPress | enum.BUTTON.A;

        modulePlayer:UpdateDoubleTapDodge( player , enum.BUTTON.A );
    end

    if signal == enum.SIGNAL.KEY_D
    then
        plrBtnPressingBit[index] = btnPress | enum.BUTTON.D;

        modulePlayer:UpdateDoubleTapDodge( player , enum.BUTTON.D );
    end
end


-- OnCutscene hookable event.
function moduleEvent:OnCutscene( player , onoff )
end
-- OnPrologue hookable event.
function moduleEvent:OnPrologue( player , onoff )
end
-- OnEpilogue hookable event.
function moduleEvent:OnEpilogue( player , onoff )
end
-- OnCredit hookable event.
function moduleEvent:OnCredit( player , onoff )
end


-- Default hook execution(s).
function Game.Rule:OnPlayerSpawn( player )
    module:OnPlayerSpawn( player );
end
function Game.Rule:OnPlayerAttack( victim , attacker , dmg , weapon , hitbox )
    return module:OnPlayerAttack( victim , attacker , dmg , weapon , hitbox );
end
function Game.Rule:OnPlayerSignal( player, signal )
    module:OnPlayerSignal( player, signal );
end
function Game.Rule:OnUpdate( time )
    module:OnUpdate( time )
end


print( "Game.COF.Main is loaded!" );

